feat: explode archive materials into named contract slots with a prefix policy selector - #3314
Conversation
A policy attachment's material selector now supports a match mode. The default (unspecified) remains an exact name match, so existing contracts are unchanged; PREFIX matches every material whose name starts with the selector name, letting a single attachment target a set of related materials (e.g. an archive exploded into "<name>", "<name>-1", …). Assisted-by: Claude Code Signed-off-by: Javier Rodriguez <javier@chainloop.dev> Chainloop-Trace-Sessions: 47ceb45b-7e6f-4843-932d-062e50b6419d
Archive explosion now names the first entry (in sorted path order) with the exact --name and the rest with positional suffixes "<name>-1", "<name>-2", …. Entries are sorted by their normalized path before naming so the file-to-name mapping is deterministic and reproducible regardless of how the archive stored them, and the first name lines up with a fixed contract's named required slot. Assisted-by: Claude Code Signed-off-by: Javier Rodriguez <javier@chainloop.dev> Chainloop-Trace-Sessions: 47ceb45b-7e6f-4843-932d-062e50b6419d
When `att add` explodes an archive, the original archive is now recorded once as an EVIDENCE material and cross-linked with every exploded material in both directions via chainloop.material.references, so the source bundle itself is attested. Assisted-by: Claude Code Signed-off-by: Javier Rodriguez <javier@chainloop.dev> Chainloop-Trace-Sessions: 47ceb45b-7e6f-4843-932d-062e50b6419d
- extract materials.NormalizeArchivePath and reuse it (safeArchivePath, ArchiveEntryBaseName, and the explode sort key), computed once per entry - drop the now-dead AllocateSequential and its seq field (explode uses AllocateNamed exclusively) - source-archive evidence: derive the name via SanitizeMaterialName (so an empty --name yields "material-archive", not "input-archive"), make it collision-safe against existing materials, and cross-link in a single loop - document that archive explode commits before the evidence step and is safe to re-run (deterministic names) - replace selectorMatches with a shared nameMatches predicate - test that an exploded first entry satisfies a required named contract slot Assisted-by: Claude Code Signed-off-by: Javier Rodriguez <javier@chainloop.dev> Chainloop-Trace-Sessions: 47ceb45b-7e6f-4843-932d-062e50b6419d
Adds match_mode to PolicyGroup.Material so a policy group can target a set of same-prefixed materials (e.g. an exploded archive), matching the standalone PolicyAttachment selector. Both match sites now share the nameMatches predicate, and InterpolateGroupMaterial carries match_mode through placeholder interpolation. Default remains exact (UNSPECIFIED), so existing groups are unchanged. Assisted-by: Claude Code Signed-off-by: Javier Rodriguez <javier@chainloop.dev> Chainloop-Trace-Sessions: 47ceb45b-7e6f-4843-932d-062e50b6419d
Adds coverage across the explode matrix: uncompressed tar; --name with multiple entries (name, name-1, name-2); --name colliding with an existing material (derived names start at -1); max-total-size rollback; a mid-stage staging failure rolling back every already-staged entry. Adds an end-to-end test for the source-archive EVIDENCE cross-link (bidirectional references). Assisted-by: Claude Code Signed-off-by: Javier Rodriguez <javier@chainloop.dev> Chainloop-Trace-Sessions: 47ceb45b-7e6f-4843-932d-062e50b6419d
…e tests PREFIX now matches the exact name or a "<name>-<suffix>" derivative (the explode naming scheme) rather than an arbitrary substring, so a selector "scan" matches "scan" and "scan-1" but not "scanner". Adds edge cases: prefix dash-boundary (standalone + group), an archive with no regular files, and user-annotation propagation to every exploded material. Assisted-by: Claude Code Signed-off-by: Javier Rodriguez <javier@chainloop.dev> Chainloop-Trace-Sessions: 47ceb45b-7e6f-4843-932d-062e50b6419d
Mirrors a multi-stage contract with two roles, each guarded by its own name-PREFIX selector: exploding a role into <role>/<role>-1/… routes only that role's policies, never the sibling role's, and unrelated names match neither. Assisted-by: Claude Code Signed-off-by: Javier Rodriguez <javier@chainloop.dev> Chainloop-Trace-Sessions: 47ceb45b-7e6f-4843-932d-062e50b6419d
- use slices.Sorted(maps.Keys(...)) for the material-name helper (drops the hand-rolled sort + import) - consolidate the two zip builders in the action test package: writeTestZip now delegates to writeZipWithFiles - hoist the loop-invariant verifier in the multi-role selector test Assisted-by: Claude Code Signed-off-by: Javier Rodriguez <javier@chainloop.dev> Chainloop-Trace-Sessions: 47ceb45b-7e6f-4843-932d-062e50b6419d
…e test Replace substring Contains checks (trivially true) with an ElementsMatch on the split reference list, so the archive's forward references must be exactly the exploded material names. Assisted-by: Claude Code Signed-off-by: Javier Rodriguez <javier@chainloop.dev> Chainloop-Trace-Sessions: 47ceb45b-7e6f-4843-932d-062e50b6419d
AI Session AnalysisMissing AI Coding SessionsWe detected commits in this PR that were AI-assisted, but the matching Chainloop Trace session(s) could not be found in Chainloop. Please make sure the AI coding session evidence has been sent by the Chainloop CLI, or add the Learn more about Chainloop Trace. Powered by Chainloop and Chainloop Trace |
There was a problem hiding this comment.
All reported issues were addressed across 19 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
Resolves the archive.go conflict against upstream's archiveio refactor (chainloop-dev#3310/chainloop-dev#3301): keep upstream's archiveio-backed primitives and re-apply the AllocateNamed allocator and NormalizeArchivePath helper on top. Regenerated the workflowcontract proto artifacts so the additive match_mode field coexists with the upstream protovalidate bump. Assisted-by: Claude Code Signed-off-by: Javier Rodriguez <javier@chainloop.dev>
…rning Addresses code-review (cubic) findings: - Fold the source-archive EVIDENCE material and its bidirectional cross-links into AddMaterialsFromArchive's single atomic commit, behind the new WithSourceArchiveEvidence option the CLI opts into. A failed evidence step now rolls the whole set back instead of leaving exploded materials that a retry would duplicate (the allocator seeds from existing names). - Correct the --policy-input-from-file warning: the runtime inputs still apply to policy evaluation of every exploded material; only the per-input evidence materials are not recorded. Assisted-by: Claude Code Signed-off-by: Javier Rodriguez <javier@chainloop.dev> Chainloop-Trace-Sessions: 47ceb45b-7e6f-4843-932d-062e50b6419d
There was a problem hiding this comment.
All reported issues were addressed across 3 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
…archive Follow-up review (cubic): the reverse-edge assignment overwrote any chainloop.material.references a caller set via runtimeAnnotations. Extract a shared materials.AppendReferences that preserves existing entries and dedupes, and route both the crafter reverse edge and the action addReference through it. Assisted-by: Claude Code Signed-off-by: Javier Rodriguez <javier@chainloop.dev> Chainloop-Trace-Sessions: 47ceb45b-7e6f-4843-932d-062e50b6419d
…lity Per review (Miguel): unprefixed values read better in contracts (match_mode: PREFIX vs MATCH_MODE_PREFIX) and match the other unprefixed enums in this file (RunnerType, MaterialType). ENUM_VALUE_PREFIX is already ignored file-wide; the bare UNSPECIFIED zero value needs a single inline buf:lint:ignore ENUM_ZERO_VALUE_SUFFIX. Regenerated pb.go/TS/jsonschema and updated the Go references. Assisted-by: Claude Code Signed-off-by: Javier Rodriguez <javier@chainloop.dev> Chainloop-Trace-Sessions: 47ceb45b-7e6f-4843-932d-062e50b6419d
There was a problem hiding this comment.
All reported issues were addressed across 10 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
|
Looks great, @javirln . Just a nit comment. |
Per review (jiparis): a name-PREFIX selector should be a literal prefix, not bounded on the "-" separator. The user provides the prefix, so silently matching "<name>-" instead of "<name>" is inconsistent with what they wrote; authors are expected to choose a discriminating prefix. Matching is now strings.HasPrefix(materialID, name). Multi-role separation still holds because distinct full-name prefixes do not cross-match. Assisted-by: Claude Code Signed-off-by: Javier Rodriguez <javier@chainloop.dev> Chainloop-Trace-Sessions: 47ceb45b-7e6f-4843-932d-062e50b6419d
Summary
Makes
chainloop attestation addusable with fixed, named contracts when a single logical input is delivered as an archive of many same-kind files (for example a bundle of SARIF reports). Today an archive can be exploded into many materials, but the resulting materials get generic auto-generated names and therefore cannot satisfy a contract's required, named material slot, nor be targeted by a name-based policy selector. This change closes that gap.Three additive, backward-compatible pieces.
1. Stable, reproducible naming for exploded materials
When
--valueis an explodable archive (.zip/.tar/.tar.gz) for an explodable kind, the CLI now:--name, and the rest<name>-1,<name>-2, …The first entry therefore fills a required, named contract slot, and the file-to-name mapping is fully reproducible for a given archive (no timestamps or randomness).
2. The source archive is recorded as evidence
The original archive is recorded once as an
EVIDENCEmaterial and cross-linked with every exploded material in both directions via thechainloop.material.referencesannotation, so the source bundle itself is attested and traceable to the materials derived from it.3. Prefix match mode for material selectors
Adds an additive
match_modeto the contract material selector —EXACT(default) andPREFIX— on both standalone policy attachments (PolicyAttachment.MaterialSelector) and policy groups (PolicyGroup.Material). A single shared predicate drives both paths.PREFIXmatches the exact name or a<name>-<suffix>derivative, so a selector can target an entire exploded set:PREFIXis a literal prefix — it matches any material whose name begins with the value — so choose a discriminating prefix (typically the full slot name, e.g.scan-report, which matchesscan-report,scan-report-1, …).How policies apply to the exploded set
A policy attachment applies to a material when the policy's declared kind matches the material's type and the selector, if present, matches the name. Evaluation is per material: N matching materials times M attachments produce N×M independent evaluations, each recorded against its own material. Exploding N reports into a prefix-selected slot therefore runs every attachment against every report. This also supports multi-slot contracts where each role has its own prefix selector (for example a build-time scan slot and a stripped-release scan slot): the two prefixes never cross-match, so each role's policies apply only to that role's materials.
What this covers
.zip,.tar, and.tar.gz; deterministic ordering;--namecollisions; user--annotations propagated to every exploded material; archive-as-evidence cross-linking in both directions.match_modekeep exact-match behavior, and adding a single (non-archive) material is unchanged.What this does not cover
.zipinside the archive is treated as a single regular file.ZAP_DAST_ZIP) are still recorded whole, not exploded.--namecollides with a material already present in the attestation, the allocator never overwrites it, so the first exploded entry becomes<name>-1and will not fill an exact required slot named<name>in that case.Compatibility
The proto change is purely additive (a new optional enum field with
UNSPECIFIED = 0decoding to exact match). Contracts are persisted as raw body plus serialized proto, so no data migration is required and existing contracts deserialize unchanged.AI assistance: Claude Code.